package com.xyhy.core.base.service;

import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;

import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cglib.beans.BeanCopier;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.domain.Persistable;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.google.common.collect.Lists;

import lombok.Getter;
import lombok.SneakyThrows;

/**
 * 业务对象的Crud基类
 *
 * @param <T>     任意JPA对象
 * @param <ID>    JPA对象的ID类型
 * @param <REPOS> JPA对象的仓库
 */
@EnableAspectJAutoProxy(exposeProxy = true)
public class CrudService<T extends Persistable<ID>, ID, REPOS extends CrudRepository<T, ID>>
        implements ICrudService<T, ID> {
  @Getter
  @Autowired
  protected REPOS repos;

  private final Class<T> clazz = getClazz();
  private final BeanCopier copier = BeanCopier.create(clazz, clazz, false);

  @SuppressWarnings("unchecked")
  private Class<T> getClazz() {
    return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
  }

  @SneakyThrows
  protected T copy(T data) {
    T result = clazz.getDeclaredConstructor().newInstance();
    copier.copy(data, result, null);
    return result;
  }

  @SuppressWarnings("unchecked")
  protected <S> S self() {
    return (S) AopContext.currentProxy();
  }

  @SuppressWarnings("unchecked")
  protected <S> S self(Class<S> clazz) {
    return (S) AopContext.currentProxy();
  }

  /**
   * 是否已经存在重复记录
   *
   * @param data
   */
  protected void assertExisted(T data) {
  }

  /**
   * 是否允许删除记录
   *
   * @param data
   */
  protected void assertDelete(T data) {
  }

  /**
   * 删除关联记录
   *
   * @param data
   */
  protected void deleteRelation(T data) {
  }

  /**
   * 保存前动作，实际用于保存的对象，可能和原始传入的数据不同，这时需要进行对象复制
   *
   * @param data
   * @return 用与保存的实际对象
   */
  protected T beforeSave(T data) {
    assertExisted(data);
    return data;
  }

  /**
   * 保存后动作
   *
   * @param result 保存后的结果
   * @param data   原始传入的数据
   * @return 用于返回的实际对象
   */
  protected T afterSave(T result, T data) {
    return result;
  }

  /**
   * 删除前动作
   *
   * @param data 需要删除的对象
   */
  protected void beforeDelete(T data) {
    assertDelete(data);
  }

  /**
   * 删除后动作
   *
   * @param data 被删除的对象
   */
  protected void afterDelete(T data) {
    deleteRelation(data);
  }

  @Override
  @Transactional
  public T save(T data) {
    final CrudService<T, ID, REPOS> self = self();
    Optional.ofNullable(data.getId()).ifPresent(id -> {
      self.findById(id);
    });
    T object = beforeSave(data);
    T result = repos.save(object);
    return self.afterSave(result, data);
  }

  @Override
  @Transactional
  public void delete(ID id) {
    final CrudService<T, ID, REPOS> self = self();
    T object = self.findById(id);
    self.beforeDelete(object);
    repos.deleteById(id);
    self.afterDelete(object);
  }

  @Override
  @Transactional
  public void deleteInBatch(Iterable<ID> ids) {
    for (ID id : ids) {
      delete(id);
    }
  }

  public boolean exists(ID id) {
    Assert.notNull(id, "id不能为空!");
    return repos.existsById(id);
  }

  public boolean exists(T data) {
    return Optional.ofNullable(data).map(x -> x.getId()).filter(x -> Objects.nonNull(x))
            .map(x -> repos.existsById(x)).orElse(false);
  }

  public boolean isNew(T data) {
    return Objects.isNull(data.getId()) || !this.exists(data);
  }

  @Override
  public List<T> findAll() {
    return Lists.newArrayList(repos.findAll());
  }

  @Override
  public T findById(ID id) {
    return this.findById(id, null);
  }

  @Override
  public T findById(ID id, T defaultVal) {
    return repos.findById(id).orElse(defaultVal);
  }

  @Override
  public T findOne(ID id) {
    String msg = "表%s对应的记录不存在；ID：[%s]";
    return repos.findById(id).orElseThrow(() -> {
      return new NoSuchElementException(String.format(msg, clazz.getName(), id.toString()));
    });
  }

}
